package service

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"github.com/golang/geo/s2"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"inventory/api/inventory"
	"inventory/internal/constant"
	"inventory/internal/dao"
	"inventory/internal/model"
	"inventory/internal/pkg/generator"
	"inventory/pkg/ecode"
	"inventory/pkg/log"
	"inventory/pkg/orm"
	"inventory/pkg/util"
	"strconv"
	"strings"
	"time"
)

// CalculateDeliverFee 配送费计算
func (s *Service) CalculateDeliverFee(ctx context.Context, req *inventory.CalculateDeliverFeeRequest) (
	reply *inventory.GetSelectionWarehouseResponse, err error) {
	reply = &inventory.GetSelectionWarehouseResponse{
		Warehouses: make([]*inventory.SimpleWarehouseView, len(req.Packages))}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		now := time.Now()
		warehouseSet := make(util.UInt32BoolMap)
		for _, p := range req.Packages {
			warehouseSet[p.Id] = true
		}
		if len(warehouseSet) == 0 {
			err = ecode.BadRequest("包裹数量不能为0")
			return
		}
		// 校验仓库
		var warehouses []*model.Warehouse
		if warehouses, err = s.dao.FindWarehousesByIDs(ctx, warehouseSet.GetKeys(), req.SgId); err != nil {
			return
		}
		warehouseMap := make(model.WarehouseMap)
		if err = warehouseMap.InitFromWarehouseArray(warehouses, req.SgId, true); err != nil {
			return
		}
		ordinaryWarehouseSet := make(util.UInt32BoolMap)
		cityWarehouseSet := make(util.UInt32BoolMap)
		ordinaryWarehouseMap := make(map[uint32]*inventory.Package)
		cityWarehouseMap := make(map[uint32]*inventory.Package)
		logger := log.FromContext(ctx)
		for index, p := range req.Packages {
			id := p.Id
			warehouse, ok := warehouseMap[id]
			if !ok {
				err = constant.ErrWarehouseNotExist
				return
			}
			reply.Warehouses[index] = &inventory.SimpleWarehouseView{
				Id:   warehouse.ID,
				Name: warehouse.Name,
			}
			if p.Skipped {
				// 跳过验算环节，配送费由来源系统自行处理
				continue
			}
			switch p.DeliverType {
			case inventory.DeliverType_CITY:
				if !warehouse.CityExpress {
					// 门店未开通同城配送 & 仓库与地址不处于同城时，直接报错
					err = constant.ErrInvalidExpressMode
					return
				}
				if _, hasWarehouse := cityWarehouseSet[id]; hasWarehouse {
					// 同一配送模式包裹重复
					err = constant.ErrInvalidPackages
					return
				}
				cityWarehouseSet[id] = true
				cityWarehouseMap[id] = p
			case inventory.DeliverType_ORDINARY:
				if !warehouse.OrdinaryExpress {
					err = constant.ErrInvalidExpressMode
					return
				}
				if _, hasWarehouse := ordinaryWarehouseSet[id]; hasWarehouse {
					// 同一配送模式包裹重复
					err = constant.ErrInvalidPackages
					return
				}
				ordinaryWarehouseSet[id] = true
				ordinaryWarehouseMap[id] = p
			case inventory.DeliverType_PICK_UP:
				if !warehouse.PickUp {
					err = constant.ErrInvalidExpressMode
					return
				}
				// 需要结合无需配送判定配送费为0
				fallthrough
			case inventory.DeliverType_SKIP:
				if p.ExpectFee != 0 {
					logger.Errorf("第%d个包裹配送费计费错误：预期0，输入%d", index, p.ExpectFee)
					err = constant.ErrInvalidExpressFee
					return
				}
			default:
				err = constant.ErrInvalidExpressMode
				return
			}
		}
		if len(cityWarehouseSet) != 0 {
			// 同城包裹配送费检查
			var (
				expressRegions []*model.ExpressCoordinates
				cityCode       int
			)
			if cityCode, err = strconv.Atoi(req.CityCode); err != nil {
				err = ecode.BadRequest("城市编码不合法: %s", req.CityCode)
				return
			}
			if expressRegions, err = s.dao.FindExpressRegionsByWarehouseIDs(
				ctx, req.SgId, uint32(cityCode), cityWarehouseSet.GetKeys()); err != nil {
				return
			}
			var matchRegions map[uint32][]*model.ExpressCoordinates
			targetLocation := s2.LatLngFromDegrees(req.Lat, req.Lng)
			targetPoint := s2.PointFromLatLng(targetLocation)
			if matchRegions, err = MatchRegionsByLatLng(expressRegions, targetPoint); err != nil {
				return
			}
			for id, p := range cityWarehouseMap {
				regions, ok := matchRegions[id]
				if !ok {
					err = constant.ErrInvalidRegion
					return
				}
				var matched = false
				for _, region := range regions {
					if p.TotalAmount >= region.StartingFee {
						if p.ExpectFee > region.ShippingFee {
							// 存在更少配送费的匹配仓库配送区域
							err = constant.ErrInvalidExpressFee
							return
						} else if p.ExpectFee == region.ShippingFee {
							matched = true
						}
					}
				}
				if !matched {
					// 没有匹配对应配送费的配送区域
					err = constant.ErrInvalidExpressFee
					return
				}
			}
		}
		if len(ordinaryWarehouseSet) > 0 {
			// 异地包裹配送费检查
			var cityCode int
			if cityCode, err = strconv.Atoi(req.CityCode); err != nil {
				return
			}
			var expressCities []*model.ExpressCity
			matchCities := make(map[uint32][]*model.ExpressCity)
			if expressCities, err = s.dao.FindExpressCitiesByCityCodesFilterWarehouseIds(
				ctx, req.SgId, []uint32{uint32(cityCode - cityCode%100)}, ordinaryWarehouseSet.GetKeys()); err != nil {
				return
			}
			for _, expressCity := range expressCities {
				matchCities[expressCity.WarehouseID] = append(matchCities[expressCity.WarehouseID], expressCity)
			}
			for id, p := range ordinaryWarehouseMap {
				expectFee := p.ExpectFee
				cities, ok := matchCities[id]
				if !ok {
					err = constant.ErrInvalidCity
					return
				}
				var matched = false
				for _, city := range cities {
					actualFee := city.FirstWeightPrice
					if p.Weight > 1000 {
						actualFee += (p.Weight - 1) / 1000 * city.AdditionalWeightPrice
					}
					if expectFee > actualFee {
						// 存在更少配送费的匹配城市区域
						err = constant.ErrInvalidExpressFee
						return
					} else if expectFee == actualFee {
						matched = true
					}
				}
				if !matched {
					// 没有匹配对应配送费的配送区域
					err = constant.ErrInvalidExpressFee
					return
				}
			}
		}
		var bodJSON []byte
		if bodJSON, err = json.Marshal(&model.CalculationBody{
			Lat:      req.Lat,
			Lng:      req.Lng,
			CityCode: req.CityCode,
			Packages: req.Packages,
		}); err != nil {
			return
		}
		var calculationRequest *model.ProcessRequest
		if calculationRequest, err = s.dao.LockCalculatedProcessRequest(ctx, req.RequestId, uint8(req.Source)); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = s.dao.Create(ctx, &model.ProcessRequest{
					RequestID:     req.RequestId,
					Source:        uint8(req.Source),
					FreezeOrderID: sql.NullInt64{},
					Body:          bodJSON,
					Status:        constant.CalculateRequestInit,
					CreateTime:    now,
					UpdateTime:    now,
				})
			}
			return
		}
		if calculationRequest.Status != constant.CalculateRequestInit {
			err = constant.ErrCalculationReused
			return
		}
		if err = s.dao.Update(ctx, &calculationRequest, map[string]interface{}{
			"body":        bodJSON,
			"update_time": time.Now(),
		}); err != nil {
			return
		}
		return
	})
	return
}

// TagOrderReady 标记已备货
func (s *Service) TagOrderReady(ctx context.Context, req *inventory.CommonOrderRequest) (resp *inventory.DefaultResp, err error) {
	empId := req.WebToken.EmpId
	resp = &inventory.DefaultResp{
		Status: true,
	}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var (
			warehouse      *model.Warehouse
			processDetails []*model.ProcessDetail
			storages       []*model.Storage
			remark         *model.ProcessOrderRemark
			remarkJSON     json.RawMessage
		)
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.DeliveryManagement); err != nil {
			return err
		}
		order := model.ProcessOrder{OrderNumber: req.OrderNumber}
		if err = s.dao.Lock(ctx, order, &order); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrOrderNotExist
			}
			return
		}
		// 状态校验
		if inventory.ProcessOrderStatus(order.Status) != inventory.ProcessOrderStatus_ProcessOrderWaitStock {
			err = constant.ErrProcessStatus
			return
		}
		if order.RefundFlag == 1 {
			err = constant.ErrOrderRefunding
			return
		}
		if err = json.Unmarshal(order.Remark, &remark); err != nil {
			return
		}
		if processDetails, err = s.dao.FindProcessOrderDetails(ctx, order.ID); err != nil {
			return
		}
		storageKeyMap := make(util.StringBoolMap)
		storageQuantityMap := make(map[string]float64)
		for _, d := range processDetails {
			if d.IsAccounting {
				storageKeyMap[d.StorageKey] = true
				// 这里需要聚合物料
				storageQuantityMap[d.StorageKey] += d.Quantity
			}
		}
		storageMap := make(model.StorageMap)
		if storages, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, req.Id, storageKeyMap.GetKeys()); err != nil {
			return
		}
		storageMap.InitStorageMap(storages)
		for key, quantity := range storageQuantityMap {
			// 由于库存已经冻结，这里不需要处理严管控场景
			if storage, ok := storageMap[key]; ok && storage.Quantity < quantity {
				err = formatStorageUnavailableError(storage)
				return
			}
		}
		now := time.Now()
		var (
			extractKey string
			count      int64
		)
		// 确认自提随机值未使用
		for {
			extractKey = fmt.Sprintf("%06d", generator.RandSource.Intn(1000000))
			if count, err = s.dao.GetProcessOrderCountByExtractKey(ctx, int(req.Id), extractKey); err != nil {
				return
			}
			if count == 0 {
				break
			}
		}
		_ = order.ExtractKey.Scan(extractKey)
		remark.ExtractKey = extractKey
		if remarkJSON, err = json.Marshal(remark); err != nil {
			return
		}
		deadlineString := now.Format(util.DateLayout) + " " + warehouse.Deadline.String()
		deadlineTime, _ := util.ParseTime(deadlineString)
		if deadlineTime.Before(now) {
			deadlineTime = deadlineTime.AddDate(0, 0, 1)
		}
		order.Remark = orm.JSON(remarkJSON)
		order.Status = uint8(inventory.ProcessOrderStatus_ProcessOrderWaitMentioned)
		order.Operator = empId
		order.PrepareTime = util.NullTime{
			Time:  now,
			Valid: true,
		}
		order.UpdateTime = now
		order.ExpectDeliverDate = util.NullTime{
			Time: time.Date(deadlineTime.Year(), deadlineTime.Month(), deadlineTime.Day(),
				0, 0, 0, 0, util.FixedLocation),
			Valid: true,
		}
		if err = s.dao.Save(ctx, order); err != nil {
			return
		}
		// 发送缺货通知
		page := fmt.Sprintf(model.PickMessagePage, order.ExtOrderNumber, req.WebToken.SgId)
		payload, _ := json.Marshal(&model.PickMessagePayLoad{
			CharacterString9:  model.DataCell{Value: order.ExtOrderNumber},
			CharacterString11: model.DataCell{Value: extractKey},
			Date2:             model.DataCell{Value: deadlineTime.Format(util.MinuteLayout)},
			Thing15: model.DataCell{Value: fmt.Sprintf(
				"请在门店%s休息前，及时到店哦", deadlineTime.Format(util.DurationWithoutSecondLayout))},
		})
		switch inventory.SourceEnum(order.Source) {
		case inventory.SourceEnum_MALL_ORDER:
			// 备货时发送备货通知
			if err = s.downstream.SendWechatTemplate(
				ctx, order.MemberID, "pick", page, string(payload)); err != nil {
				return err
			}
		}
		statsMsg, _ := json.Marshal(&inventory.InventoryOrderUpdatePayload{
			OrderNumber:    order.OrderNumber,
			ExtOrderNumber: order.ExtOrderNumber,
			Source:         inventory.SourceEnum(order.Source),
			Timestamp:      now.Unix(),
			Status:         inventory.ProcessOrderStatus(order.Status),
			DeliverType:    inventory.DeliverType(order.DeliverType),
			DeliverMemo: &inventory.InventoryOrderUpdatePayload_DeliverMemo{
				ExtractKey:    extractKey,
				DeadlineTime:  deadlineTime.Format(util.MinuteLayout),
				PickUpAddress: warehouse.Address,
				PrepareTime:   now.Format(util.DefaultLayout),
			},
		})
		if err = s.dao.PublishUniqueNSQMessage(ctx, constant.TopicInventoryStatus, statsMsg); err != nil {
			return err
		}
		return
	})
	return
}

// NotifyOrderLack 订单缺货通知
func (s *Service) NotifyOrderLack(ctx context.Context, req *inventory.CommonOrderRequest) (resp *inventory.DefaultResp, err error) {
	empId := req.WebToken.EmpId
	resp = &inventory.DefaultResp{
		Status: true,
	}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var warehouse *model.Warehouse
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.DeliveryManagement); err != nil {
			return err
		}
		var (
			processDetails []*model.ProcessDetail
			storages       []*model.Storage
		)
		now := time.Now()
		order := model.ProcessOrder{OrderNumber: req.OrderNumber}
		if err = s.dao.Lock(ctx, order, &order); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrOrderNotExist
			}
			return
		}
		// 状态校验
		orderStatus := inventory.ProcessOrderStatus(order.Status)
		if orderStatus != inventory.ProcessOrderStatus_ProcessOrderWaitStock && orderStatus != inventory.ProcessOrderStatus_ProcessOrderWaitShip {
			err = constant.ErrProcessStatus
			return
		}
		if order.RefundFlag == 1 {
			err = constant.ErrOrderRefunding
			return
		}
		if order.LastNotifyTime.Valid && now.Sub(order.LastNotifyTime.Time).Hours() < 24 {
			// 上次通知24小时内不必重试
			err = ecode.AppendMessage(constant.ErrForbiddenAction, fmt.Sprintf(
				"当前订单上次缺货通知触发时间为%s, 不满足24小时", order.LastNotifyTime.Time.Format(util.DurationLayout)))
			return
		}
		if processDetails, err = s.dao.FindProcessOrderDetails(ctx, order.ID); err != nil {
			return
		}
		storageKeyMap := make(util.StringBoolMap)
		storageQuantityMap := make(map[string]float64)
		detailIDMap := make(map[uint64]*model.ProcessDetail)
		targetDetailMap := make(map[string]map[string]*model.ProcessDetail)
		for _, d := range processDetails {
			detailIDMap[d.ID] = d
			if d.IsAccounting {
				storageKeyMap[d.StorageKey] = true
				// 这里需要聚合物料
				storageQuantityMap[d.StorageKey] += d.Quantity
				if _, ok := targetDetailMap[d.StorageKey]; !ok {
					targetDetailMap[d.StorageKey] = make(map[string]*model.ProcessDetail)
				}
				if d.ParentID == 0 {
					targetDetailMap[d.StorageKey][d.StorageKey] = d
				} else {
					targetDetailMap[d.StorageKey][detailIDMap[d.ParentID].StorageKey] = detailIDMap[d.ParentID]
				}
			}
		}
		storageMap := make(model.StorageMap)
		if storages, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, req.Id, storageKeyMap.GetKeys()); err != nil {
			return
		}
		storageMap.InitStorageMap(storages)
		lackMap := make(map[string]*model.ProcessDetail)
		for key, quantity := range storageQuantityMap {
			if storage, ok := storageMap[key]; ok && storage.Quantity < quantity {
				for targetKey, targetDetail := range targetDetailMap[key] {
					lackMap[targetKey] = targetDetail
				}
			} else if warehouse.StorageMode == constant.WarehouseStorageModeStrong {
				for targetKey, targetDetail := range targetDetailMap[key] {
					lackMap[targetKey] = targetDetail
				}
			}
		}
		if len(lackMap) == 0 {
			// 不符合缺货状态，无需通知
			err = constant.ErrNeedRefresh
			return
		}
		var targetMemo []string
		for _, targetDetail := range lackMap {
			detailRemark := &model.SnapShotRemark{}
			if err = json.Unmarshal(targetDetail.Remark, &detailRemark); err != nil {
				return
			}
			targetMemo = append(targetMemo, fmt.Sprintf("%s*%d", detailRemark.Name, int(targetDetail.Quantity)))
			targetDetail.NotifyFlag = true
			targetDetail.UpdateTime = now
			if err = s.dao.Save(ctx, &targetDetail); err != nil {
				return
			}
		}
		memo := strings.Join(targetMemo, ",")
		if len(memo) > 50 {
			memo = memo[0:47] + "..."
		}
		_ = order.LastNotifyTime.Scan(now)
		order.Operator = empId
		order.UpdateTime = now
		if err = s.dao.Save(ctx, order); err != nil {
			return
		}
		// 发送缺货通知
		// 发送缺货通知
		page := fmt.Sprintf(model.LackMessagePage, order.ExtOrderNumber, req.WebToken.SgId)
		payload, _ := json.Marshal(&model.LackMessagePayLoad{
			Thing4:           model.DataCell{Value: "抱歉，您预订的商品有部分缺货，请确认！"},
			CharacterString2: model.DataCell{Value: order.ExtOrderNumber},
			Thing1:           model.DataCell{Value: memo},
			Phrase6:          model.DataCell{Value: "缺货中"},
		})
		switch inventory.SourceEnum(order.Source) {
		case inventory.SourceEnum_MALL_ORDER:
			// 备货时发送备货通知
			if err = s.downstream.SendWechatTemplate(
				ctx, order.MemberID, "pick", page, string(payload)); err != nil {
				return err
			}
		default:
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "只有商城订单能够发送缺货通知")
			return
		}
		return
	})
	return
}

// deliverProcessStorage 履约单扣库操作
func deliverProcessStorage(ctx context.Context, dao *dao.Dao, order *model.ProcessOrder,
	memo *inventory.InventoryOrderUpdatePayload_DeliverMemo,
	now time.Time, allowStorageError bool) (err error) {
	var (
		processDetails []*model.ProcessDetail
		storages       []*model.Storage
		freezeOrder    *model.FreezeOrder
	)
	if freezeOrder, err = dao.GetFreezeOrderByID(ctx, uint64(order.FreezeOrderID.Int64)); err != nil {
		return
	}
	if freezeOrder.Status != constant.FreezeOrderProcessing {
		err = constant.ErrInvalidFreezeOrder
		return
	}
	if processDetails, err = dao.FindProcessOrderDetails(ctx, order.ID); err != nil {
		return
	}
	storageKeyMap := make(util.StringBoolMap)
	storageQuantityMap := make(map[string]float64)
	unfreezeMap := make(map[string]float64)
	for _, d := range processDetails {
		if d.IsAccounting {
			storageKeyMap[d.StorageKey] = true
			// 这里需要聚合物料
			storageQuantityMap[d.StorageKey] += d.Quantity
		}
		unfreezeMap[d.StorageKey] = d.Quantity
	}
	if allowStorageError {
		storageMap := make(model.StorageMap)
		if storages, err = dao.FindWarehouseStoragesByStorageKeys(
			ctx, order.WarehouseID, storageKeyMap.GetKeys()); err != nil {
			return
		}
		storageMap.InitStorageMap(storages)
		for key, quantity := range storageQuantityMap {
			if storage, ok := storageMap[key]; ok {
				if storage.Quantity < quantity {
					err = formatStorageUnavailableError(storage)
					return
				}
			}
		}
	}
	// 更新冻结单
	if err = updateFreezeOrder(ctx, dao, freezeOrder, order, unfreezeMap, now, true); err != nil {
		return
	}
	order.UpdateTime = now
	_ = order.DeliverTime.Scan(now)
	if err = dao.Save(ctx, order); err != nil {
		return
	}
	statsMsg, _ := json.Marshal(&inventory.InventoryOrderUpdatePayload{
		OrderNumber:    order.OrderNumber,
		ExtOrderNumber: order.ExtOrderNumber,
		Source:         inventory.SourceEnum(order.Source),
		Timestamp:      now.Unix(),
		Status:         inventory.ProcessOrderStatus(order.Status),
		DeliverType:    inventory.DeliverType(order.DeliverType),
		DeliverMemo:    memo,
	})
	if err = dao.PublishUniqueNSQMessage(ctx, constant.TopicInventoryStatus, statsMsg); err != nil {
		return err
	}
	// 发货时，由于库存仅从冻结部分扣除，不影响外部销售库存，无需库存通知
	return
}

// DeliveryOrder 订单发货（库存扣减）
func (s *Service) DeliveryOrder(ctx context.Context, req *inventory.DeliveryProcessOrderRequest) (resp *inventory.DefaultResp, err error) {
	var (
		warehouse    *model.Warehouse
		platform     *model.ExpressPlatform
		order        model.ProcessOrder
		expressOrder *model.ExpressOrder
		config       *model.ExpressConfig
		address      *inventory.AddressInfo
	)
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	// 前置校验
	if config, err = s.dao.GetExpressConfigByPlatformID(ctx, sgId, req.Id, req.PlatformId); err != nil {
		if errors.Cause(err) == gorm.ErrRecordNotFound {
			err = ecode.BadRequest("所选配送平台未配置，不能用于发起配送")
		}
		return
	}
	switch req.DeliverType {
	case inventory.DeliverType_ORDINARY, inventory.DeliverType_CITY:
		if platform, err = s.dao.GetPlatform(ctx, req.PlatformId); err != nil {
			return
		}
		if inventory.DeliverType(platform.Mode) == inventory.DeliverType_ORDINARY && req.PlatformOrderNumber == "" {
			err = ecode.BadRequest("缺少普通快递平台单号")
			return
		}
	}
	now := time.Now()
	// 创建配送单
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.DeliveryManagement); err != nil {
			return
		}
		// 锁定履约单
		order = model.ProcessOrder{OrderNumber: req.OrderNumber}
		if err = s.dao.Lock(ctx, order, &order); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrOrderNotExist
			}
			return
		}
		if inventory.ProcessOrderStatus(order.Status) != inventory.ProcessOrderStatus_ProcessOrderWaitShip {
			err = constant.ErrProcessStatus
			return
		}
		if platform != nil && platform.Mode != order.DeliverType {
			err = ecode.BadRequest("订单配送方式不合法")
			return
		}
		if order.RefundFlag == 1 {
			err = constant.ErrOrderRefunding
			return
		}
		order.ActualDeliverFee = int64(req.PlatformDeliverFee)
		order.Operator = empId
		// 检查是否存在正在呼叫的单号
		if order.ExpressOrderID.Valid {
			if expressOrder, err = s.dao.GetExpressOrderByID(ctx, uint64(order.ExpressOrderID.Int64)); err != nil {
				return
			}
			// 判断已存在配送单状态
			switch inventory.DeliverStatus(expressOrder.Status) {
			case inventory.DeliverStatus_ExpressOrderPending, inventory.DeliverStatus_ExpressOrderWaitPickup,
				inventory.DeliverStatus_ExpressOrderPickup, inventory.DeliverStatus_ExpressOrderDelivering,
				inventory.DeliverStatus_ExpressOrderReceived:
				return ecode.BadRequest("订单已经进入配送流程")
			case inventory.DeliverStatus_ExpressOrderCanceled, inventory.DeliverStatus_ExpressOrderFailed:
				// 配送失败 重新生成配送单
				expressOrder.ID = 0
				expressOrder.OutTradeNo = sql.NullString{}
				expressOrder.PlatformID = req.PlatformId
			case inventory.DeliverStatus_ExpressOrderCalling: // 调用环节异常，重试调用环节即可，无需重新落地
				if expressOrder.PlatformID != 0 && expressOrder.PlatformID != req.PlatformId {
					if platform, err = s.dao.GetPlatform(ctx, expressOrder.PlatformID); err != nil {
						return
					}
					err = ecode.BadRequest("订单正在使用%s配送调用中，并未得到平台失败推送，不能取消；请使用%s重试发起配送",
						platform.Name, platform.Name)
					return
				}
			default:
				return ecode.AppendMessage(constant.ErrSystemError, "配送订单状态校验异常")
			}
		} else {
			expressOrder = &model.ExpressOrder{
				OrderType:   1,
				OutTradeNo:  sql.NullString{},
				OrderNumber: order.OrderNumber,
				PlatformID:  req.PlatformId,
				WarehouseID: order.WarehouseID,
				Lat:         warehouse.Lat,
				Lng:         warehouse.Lng,
				DeliverFee:  req.PlatformDeliverFee,
				TradeNo:     req.PlatformOrderNumber,
				CreateTime:  now,
				UpdateTime:  now,
			}
		}
		switch req.DeliverType {
		case inventory.DeliverType_CITY:
			// 同城模式，需要生成调用平台out_trade_no
			expressOrder.Status = uint8(inventory.DeliverStatus_ExpressOrderCalling)
			if !expressOrder.OutTradeNo.Valid {
				// 生成平台调用单号
				var outTradeNo string
				if outTradeNo, err = generator.GenerateOrderNumber(now, constant.OrderTypeExpress, empId); err != nil {
					return
				}
				expressOrder.OutTradeNo = sql.NullString{
					String: outTradeNo,
					Valid:  true,
				}
			}
			address = &inventory.AddressInfo{}
			_ = json.Unmarshal(order.AddressInfo, address)
			switch platform.ID {
			case constant.ExpressDada:
				// 校验达达配置
				if len(strings.Split(config.Info, ",")) != 2 {
					err = ecode.BadRequest("达达配置错误，请联系客服处理")
					return
				}
			case constant.ExpressShunfeng:
				// NOTE: 增加平台单号前缀，用于pos区分订单来源转发
				expressOrder.OutTradeNo.String = "SF" + expressOrder.OutTradeNo.String
			default:
				err = ecode.AppendMessage(constant.ErrSystemError, "系统错误: 所选平台同城配送接口未对接")
				return
			}
			if expressOrder.ID == 0 {
				// 未创建/配送已失败时  => 新建配送单；其他~不更新配送单
				if err = s.dao.Create(ctx, &expressOrder); err != nil {
					return
				}
				order.UpdateTime = now
				order.ExpressOrderID = sql.NullInt64{
					Int64: int64(expressOrder.ID),
					Valid: true,
				}
				order.ExpressPlatformID = sql.NullInt64{
					Valid: true, Int64: int64(req.PlatformId)}
				// 订单也要前置保存，但不更新状态，确保配送费&操作人信息落地
				if err = s.dao.Save(ctx, &order); err != nil {
					return
				}
			}
		case inventory.DeliverType_ORDINARY, inventory.DeliverType_SKIP:
			// 异地模式
			var platformName string
			if expressOrder.ID != 0 {
				err = ecode.AppendMessage(constant.ErrSystemError, "配送单已存在，无需重新发起")
				return
			}
			expressOrder.Status = uint8(inventory.DeliverStatus_ExpressOrderDelivering)
			order.Status = uint8(inventory.ProcessOrderStatus_ProcessOrderShipped)
			if err = s.dao.Create(ctx, &expressOrder); err != nil {
				return
			}
			order.ExpressOrderID = sql.NullInt64{
				Int64: int64(expressOrder.ID),
				Valid: true,
			}
			order.ExpressPlatformID = sql.NullInt64{
				Valid: true, Int64: int64(req.PlatformId)}
			order.DeliverType = uint8(req.DeliverType)
			if req.DeliverType == inventory.DeliverType_ORDINARY {
				platformName = platform.Name
			} else {
				platformName = "商家配送/无需配送"
			}
			var prepareTime string
			if order.PrepareTime.Valid {
				prepareTime = order.PrepareTime.Time.Format(util.DefaultLayout)
			}
			if err = deliverProcessStorage(ctx, s.dao, &order, &inventory.InventoryOrderUpdatePayload_DeliverMemo{
				Platform:         platformName,
				PlatformOrderNum: expressOrder.TradeNo,
				DeliverTime:      now.Format(util.DefaultLayout),
				PrepareTime:      prepareTime,
			}, now, true); err != nil {
				return
			}
		default:
			err = ecode.BadRequest("当前订单不支持配送")
			return
		}
		return
	})
	if err != nil {
		return
	}
	// 同城配送后置处理
	if req.DeliverType == inventory.DeliverType_CITY {
		switch platform.ID {
		case constant.ExpressShunfeng: // 顺丰
			expressOrder.TradeNo, expressOrder.DeliverFee, err = s.dao.SFDelivery(
				ctx, config.Info, expressOrder.OutTradeNo.String, order, warehouse.Name)
		case constant.ExpressDada: // 达达
			c := strings.Split(config.Info, ",")
			expressOrder.DeliverFee, err = s.dao.DadaDelivery(
				ctx, c[0], c[1], expressOrder.OutTradeNo.String, float64(order.ProdTrans)/100, address,
				warehouse.RegionCode)
		default:
			err = ecode.AppendMessage(constant.ErrSystemError, "系统错误: 所选平台同城配送接口未对接")
			return
		}
		// 标记配送失败
		if err != nil {
			expressOrder.Status = uint8(inventory.DeliverStatus_ExpressOrderFailed)
			expressOrder.Reason = sql.NullString{
				String: err.Error(),
				Valid:  true,
			}
			expressOrder.UpdateTime = now
			// 这里不能落地，订单未锁定，不能直接更新
		}
		err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
			// 锁定order
			if err = s.dao.Lock(ctx, order, &order); err != nil {
				return
			}
			if inventory.ProcessOrderStatus(order.Status) != inventory.ProcessOrderStatus_ProcessOrderWaitShip {
				// 配送信息已在推送环节处理，这里不报错，直接返回即可
				return
			}
			if inventory.DeliverStatus(expressOrder.Status) == inventory.DeliverStatus_ExpressOrderFailed {
				// 配送失败，不更新履约单
				if err = s.dao.Save(ctx, expressOrder); err != nil {
					return
				}
			} else {
				// 已配送待接单
				expressOrder.Status = uint8(inventory.DeliverStatus_ExpressOrderPending)
				order.Status = uint8(inventory.ProcessOrderStatus_ProcessOrderShipped)
				if err = s.dao.Save(ctx, expressOrder); err != nil {
					return
				}
				var prepareTime string
				if order.PrepareTime.Valid {
					prepareTime = order.PrepareTime.Time.Format(util.DefaultLayout)
				}
				if err = deliverProcessStorage(ctx, s.dao, &order, &inventory.InventoryOrderUpdatePayload_DeliverMemo{
					Platform:         platform.Name,
					PlatformOrderNum: expressOrder.TradeNo,
					DeliverTime:      now.Format(util.DefaultLayout),
					PrepareTime:      prepareTime,
				}, now, true); err != nil {
					return
				}
			}
			return
		})
		if inventory.DeliverStatus(expressOrder.Status) == inventory.DeliverStatus_ExpressOrderFailed {
			// 配送失败时，在事务外将错误抛回web，确保用户可见
			err = ecode.AppendMessage(constant.ErrRemoteAPIFailed,
				fmt.Sprintf("顺丰配送失败: %s", expressOrder.Reason.String))
			return
		}
	}
	resp = &inventory.DefaultResp{
		Status: true,
	}
	return
}

// PickUpOrder 订单自提/免配送模式发货
func (s *Service) PickUpOrder(ctx context.Context, req *inventory.CommonOrderRequest) (resp *inventory.DefaultResp, err error) {
	empId := req.WebToken.EmpId
	resp = &inventory.DefaultResp{
		Status: true,
	}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var warehouse *model.Warehouse
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.DeliveryManagement); err != nil {
			return
		}
		order := model.ProcessOrder{}
		if err = s.dao.Lock(ctx, model.ProcessOrder{OrderNumber: req.OrderNumber}, &order); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrOrderNotExist
			}
			return
		}
		if inventory.ProcessOrderStatus(order.Status) != inventory.ProcessOrderStatus_ProcessOrderWaitMentioned {
			err = constant.ErrProcessStatus
			return
		}
		if order.RefundFlag == 1 {
			err = constant.ErrOrderRefunding
			return
		}
		now := time.Now()
		order.Status = uint8(inventory.ProcessOrderStatus_ProcessOrderReceived)
		_ = order.ExtractKey.Scan(nil)
		deadlineTime, _ := warehouse.Deadline.Value()
		var prepareTime string
		if order.PrepareTime.Valid {
			prepareTime = order.PrepareTime.Time.Format(util.DefaultLayout)
		}
		if err = deliverProcessStorage(ctx, s.dao, &order, &inventory.InventoryOrderUpdatePayload_DeliverMemo{
			ExtractKey:    fmt.Sprint(order.ExtractKey.String),
			DeadlineTime:  fmt.Sprintf("%s %s", now.Format(util.DateLayout), deadlineTime),
			PickUpAddress: warehouse.Address,
			DeliverTime:   now.Format(util.DefaultLayout),
			PrepareTime:   prepareTime,
		}, now, true); err != nil {
			return
		}
		return
	})
	return
}
